/*
 *  Copyright 2009 Honiewelle Flores 
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License"); 
 *  you may not use this file except in compliance with the License. 
 *  You may obtain a copy of the License at 
 *  
 *  	http://www.apache.org/licenses/LICENSE-2.0 
 *  
 *  Unless required by applicable law or agreed to in writing, software 
 *  distributed under the License is distributed on an "AS IS" BASIS, 
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 *  See the License for the specific language governing permissions and 
 *  limitations under the License. 
 */

package com.einox.chikko;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author Honiewelle Flores
 *
 * Chikka Protocol
 * For educational purposes only. 
 */

public class Chikko implements Serializable{

	private static final long serialVersionUID = 1L;
	private static final String SERVER_ADDRESS = "209.10.203.102";
	private static final int SERVER_PORT = 6301;
	
	private static final int OP_PROFILE = 41;
	private static final int OP_VERSION = 43;
	private static final int OP_LOGIN = 1;
	private static final int OP_LOGOUT = 2;
	private static final int OP_SENDMESSAGE = 14;
	private static final int OP_RECEIVEMESSAGE = 14;
	private static final int OP_GETPROFILE = 70;
	private static final int OP_ALIVE = 40;
	private static final int OP_GETCREDITS = 23; 
	private static final int OP_RECEIVEACK = 89;
	private static final int OP_BUDDYSTATUS = 18;
	private static final int OP_BUDDYIDLE = 22;
	private static final int OP_BUDDYDELETE = 6;
	private static final int OP_BUDDYADDNOTIFY = 6;
	private static final int OP_SUCCESS = 50;
	private static final int OP_FORGOT = 11;
	private static final int OP_CREDIT = 23;
	public static final int FORGOT_EMAIL = 2;
	public static final int FORGOT_MOBILE = 1;
	private Socket socket;
		
	private Buddy profile;
	private List<Buddy> buddies;
	private boolean isLoggedIn;
	
	private ChikkoEventListener listener;
	
	private Thread tServerListener;
	
	public Chikko() throws ChikkoException{
		socket = connect();
	}
	
	/**
	 * 
	 * @param username			Username
	 * @param password			Password
	 * @throws ChikkoException	If something goes wrong.
	 */
	public void login(String username, String password) throws ChikkoException{
		username = username.trim();
		password = password.trim();
		
		if (username.equals("") || password.equals("")){
			throw new ChikkoException("Username and password is required.");
		}
		
		Packet packet = new Packet();
		packet.setOperationCode(OP_LOGIN);
		packet.addParameter(3, "9");
		packet.addParameter(4, "8");
		packet.addParameter(2, password);
		
		if (username.contains("@")){
			packet.addParameter(5, username);
		}
		else {
			packet.addParameter(1, username);
		}
		
		try {
			Chikko.sendPacketReply(socket,packet);
			packet = Chikko.getServerPacketReply(socket);
			
			if (packet.getOperationCode()-OP_SUCCESS != OP_LOGIN){
				throw new ChikkoException("Login failed");
			}
			
			// GET USER INFO
			packet = Chikko.getServerPacketReply(socket);
			
			profile = new Buddy(packet.getParameter(1));
			profile.setFirstName(packet.getParameter(16));
			profile.setLastName(packet.getParameter(17));
			profile.setEmail(packet.getParameter(18));
			profile.setCountry(packet.getParameter(26));
			profile.setCarrier(packet.getParameter(7));
			
			// SEND ACK 
			
			packet = new Packet(packet.getOperationCode());
			packet.setOperationCode(OP_PROFILE + OP_SUCCESS);
			
			Chikko.sendPacketReply(socket, packet);
			
			// RECEIVE NUMBER OF BUDDIES
						
			packet = Chikko.getServerPacketReply(socket);
			int buddyCount = Integer.parseInt(packet.getParameter(2));
			
			// GET BUDDIES
			
			buddies = new ArrayList<Buddy>();
			
			while (buddies.size() < buddyCount){
				List<Packet> packets = Chikko.getServerPacketReplies(socket);
				for (Packet i : packets){
					
					if (i.getOperationCode() != 31) break;
					Buddy buddy = new Buddy(i.getParameter(10));
					buddy.setScreenName(i.getParameter(12));
					buddy.setCarrier(i.getParameter(7));
					buddy.setId(Integer.parseInt(i.getParameter(15)));
					
					buddies.add(buddy);
				}
			}
			
			
			// SEND ACK 
			// BUDDY LIST RECEIPT ACK
			
			packet = new Packet(2);
			packet.setOperationCode(OP_RECEIVEACK);
			
			Chikko.sendPacketReply(socket, packet);
			
			isLoggedIn = true;
			
			// START THE CHIKKO EVENTS THREAD
			tServerListener = new Thread(new ServerListener());
			tServerListener.start();
			
		} catch (IOException e) {
			throw new ChikkoException(e);
		}

	}
	
	public void logout(){
		if (isLoggedIn){
			Packet packet = new Packet();
			packet.setOperationCode(OP_LOGOUT);
			try {
				Chikko.sendPacketReply(socket, packet);
			} catch(IOException e){}
		}
	}
	/**
	 * Returns a list containing chikka buddies.
	 * @return
	 * @throws ChikkoException 	If something goes wrong.
	 */
	public List<Buddy> getBuddies() throws ChikkoException{
		
		if (!isLoggedIn){
			throw new ChikkoException("Not logged in.");
		}
	
		return buddies;
	}
	
	public boolean addBuddy(Buddy buddy){
		// TODO:
		return true;
	}
	
	public boolean searchBuddy(Buddy buddy){
		// TODO:
		return true;
	}
	
	public boolean blockBuddy(Buddy buddy){
		// TODO:
		return true;
	}
	
	public boolean unblockBuddy(Buddy buddy){
		// TODO:
		return true;
	}
	
	public void setStatus(int status) throws ChikkoException {
		// TODO:
	}
	
	public void getMessageHistory() throws ChikkoException {
		// TODO
	}
	
	/**
	 * 
	 * @param message	Chikko message.
	 * @return			True if successfully sent
	 * @throws ChikkoException	If something goes wrong
	 */
	public boolean sendMessage(Message message) throws ChikkoException{
		
		if (!isLoggedIn){
			throw new ChikkoException("Not logged in.");
		}
		
		Packet packet = new Packet();
		packet.setOperationCode(OP_SENDMESSAGE);
		
		packet.addParameter(30, "1");
		packet.addParameter(21, message.getBuddy().getId() + "");
		packet.addParameter(32, message.getMessage());
		
		synchronized(this){
			try {
				Chikko.sendPacketReply(socket, packet);
				packet = Chikko.getServerPacketReply(socket);
				
				if (packet != null && packet.getOperationCode() == (OP_SENDMESSAGE + OP_SUCCESS)){
					return true;
				}
			}
			catch(IOException e ){
				throw new ChikkoException(e);
			}
		}
		return false;
	}
	
	/**
	 * Checks if user is logged in.
	 * @return	Returns true if user is logged in.
	 */
	public boolean isLoggedIn() {
		return isLoggedIn;
	}
	
	/**
	 * Monitors events triggered by packets coming from the server
	 * @param listener Chikko event listener object
	 */
	public void setChikkoEventListener(ChikkoEventListener listener){
		this.listener = listener;
	}
	
	/**
	 * 	Removes the Chikko server listener
	 */
	public void removeChikkoEventListener(){
		listener = null;
	}
	
	/**
	 * 
	 * @param username	Username/mobile number of target user.
	 * @param sendTo	Send to Mobile / Email
	 * @return			Success message from server
	 * @throws ChikkoException 	If an error was returned by the server
	 */
	public static String forgotPassword(String username, int sendTo) throws ChikkoException{
		
		Socket socket = connect();
		Packet packet = new Packet();
		String result;
		
		try {
			
			// LOGIN
			packet.setOperationCode(OP_LOGIN);
			packet.addParameter(3, "9");
			packet.addParameter(4, "4");
			packet.addParameter(2, "#");
			packet.addParameter(1, "#");
			Chikko.sendPacketReply(socket, packet);
			
			packet = Chikko.getServerPacketReply(socket);
			if (packet.getOperationCode() != OP_LOGIN + OP_SUCCESS){
				throw new ChikkoException(packet.getParameter(32));
			}
			
			// SEND FORGOT PACKET
			packet = new Packet();
			packet.setOperationCode(OP_FORGOT);
			
			packet.addParameter(2, username);
			packet.addParameter(3, sendTo + "");
			packet.addParameter(1, sendTo + "");
			
			if (sendTo < 0 || sendTo > 2){
				throw new ChikkoException("Invalid send to option.");
			}
			
			Chikko.sendPacketReply(socket, packet);
			
			packet = Chikko.getServerPacketReply(socket);
			if (packet.getOperationCode() != OP_FORGOT + OP_SUCCESS){
				throw new ChikkoException(packet.getParameter(32));
			}
			
			// SERVER REPLY
			result = packet.getParameter(32); 
			
			// LOGOUT
			packet = new Packet();
			packet.setOperationCode(OP_LOGOUT);
			Chikko.sendPacketReply(socket, packet);
		}
		catch(IOException e){
			throw new ChikkoException(e);
		}
		
		return result;
	}
	
	public List<Credit> getCredits() throws ChikkoException{
		List<Credit> result = new ArrayList<Credit>();
		Packet packet = new Packet();
		
		try {
			packet.setOperationCode(OP_CREDIT); 
			Chikko.sendPacketReply(socket, packet);
			
			packet = Chikko.getServerPacketReply(socket);
			if (packet.getOperationCode() != OP_CREDIT + OP_SUCCESS){
				throw new ChikkoException("Unable to get credits. Invalid reply.");
			}
			
			List<Packet> packets = Chikko.getServerPacketReplies(socket);
			
			for (Packet i : packets){
				if (i.getOperationCode() == 47){
					Credit cdt = new Credit(i.getParameter(40));
					cdt.setAlotted(Integer.parseInt(i.getParameter(41)));
					cdt.setUsed(Integer.parseInt(i.getParameter(42)));
					result.add(cdt);
				}
			}
			
			packet = new Packet();
			packet.setOperationCode(89); 
			Chikko.sendPacketReply(socket, packet);
			
			return result;
		
		}
		catch(IOException e){
			throw new ChikkoException("Unable to get credits.", e);
		}
	}
	
	/*
	 *  Private methods
	 */
	
	private static Socket connect() throws ChikkoException{
		
		try{
//			remoteSocket = new Socket(SERVER_ADDRESS,SERVER_PORT);
			Socket socket = new Socket(InetAddress.getByName(SERVER_ADDRESS), SERVER_PORT,
						InetAddress.getLocalHost(), 4580 );
			
			while(socket.isConnected() == false){}
			
			String strReply =  Chikko.getServerReply(socket);
			String serverVersion = strReply.split(" ")[0];
			
			// SEND VERSION PACKET
			Packet packet = new Packet();
			
			packet.setOperationCode(OP_VERSION);
			packet.addParameter(1, "9");
			packet.addParameter(2, "v04.02.00.001");
			packet.addParameter(3, "v01.01.02.000");
			
			Chikko.sendPacketReply(socket, packet);
			
			packet = Chikko.getServerPacketReply(socket);
			
			if (serverVersion.equals("CTPv1.2") == false){
				throw new ChikkoException("Unsupported version: " + serverVersion);
			}
			
			return socket;
		}
		catch(IOException e){
			throw new ChikkoException("Cannot connect to server.",e);
		}
	}
	
	private static Packet getServerPacketReply(Socket socket) throws IOException{
		byte [] rawReply = Chikko.getRawServerReply(socket);
		
		int len = 0;
		
//		System.out.println("IN  : " + new String(rawReply));
		
		// CHECK IF WE HAVE A VALID START BYTE
		if (rawReply[0] != 0x2){
			throw new IOException("Not a valid packet reply, packet start byte not valid.");
		}
		
		for (int ctr = 0; ctr < rawReply.length; ctr++){
			len++;
			if (rawReply[len-1] == 0x3) break; // END OF PACKET
		}
		
		byte [] reply = Arrays.copyOfRange(rawReply, 0, len);
		
		if (reply[reply.length-1] == 0x03){
			Packet result = null;
			
			try {
				result = new Packet(new String(reply).trim());
			} catch(InvalidChikkoPacketException e){
				throw new IOException(e);
			}
						
			return result;
		}
		else {
			throw new IOException("Not a valid packet reply, packet end not found.");
		}	
	}
	
	
	
	private static List<Packet> getServerPacketReplies(Socket socket) throws IOException{
		byte [] rawReply = getRawServerReply(socket);
		List<Packet> result = new ArrayList<Packet>();
		
		
		try {
			for (String part : new String(rawReply).split("\u0003\u0002")) {
				result.add(new Packet(part.trim()));
			}
		} catch (InvalidChikkoPacketException e) {
			throw new IOException(e);
		}
		return result;
	}
	
	private static String getServerReply(Socket socket) throws IOException{
		return new String(getRawServerReply(socket));
	}
	
	private static byte [] getRawServerReply(Socket socket) throws IOException {
		byte [] result = new byte [4096]; // 4 kB
		byte [] buffer = new byte [128]; 
		int len;
		int offset=0; 
		
		BufferedInputStream in = new BufferedInputStream(socket.getInputStream());
		
		
		while ((len = in.read(buffer,0,128)) > 0){
			for (int ctr =0; ctr < len; ctr++){
				result[offset+ctr] = buffer[ctr];
			}
			if (len < 128) break;
			offset += 128;
		}
		
				
		return result;
	}
	
	private static void sendPacketReply(Socket socket, Packet packet) throws IOException{
			sendRawReply(socket,packet.toString().getBytes());
	}
	
	private static void sendRawReply(Socket socket, byte [] message) throws IOException{
		BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());
		out.write(message);
		out.flush();
	}
	
	private Buddy getBuddyById(int id){
		for (Buddy i : buddies){
			if (i.getId() == id){
				return i;
			}
		}
		
		return null;
	}
	
	private class ServerListener implements Runnable {
		public void run(){
			try {
				Thread.sleep(10000);
			}
			catch (InterruptedException e){}
			
			
			while(true){
				try {
					Thread.sleep(4000);
				} catch(InterruptedException e){}
				
//				System.out.println("Listening");
				
				synchronized(Chikko.this){
					try {				
						socket.setSoTimeout(1000);
						Packet packet = Chikko.getServerPacketReply(Chikko.this.socket);
						if (packet.getOperationCode() == OP_ALIVE){
							packet.setOperationCode(OP_ALIVE + OP_SUCCESS);
							Chikko.sendPacketReply(socket, packet);
						}
						else if (packet.getOperationCode() == OP_RECEIVEMESSAGE){
							Message message =  new Message(packet.getParameter(32), 
									getBuddyById(Integer.parseInt(packet.getParameter(15))));
							message.setTimestamp(packet.getParameter(22));
							
							packet = new Packet(packet.getPacketNumber());
							packet.setOperationCode(OP_RECEIVEMESSAGE + OP_SUCCESS);
							packet.addParameter(3, "");
							Chikko.sendPacketReply(socket, packet);
							
							listener.onMessageReceive(message);
						}
						else if (packet.getOperationCode() == OP_BUDDYSTATUS){
							Buddy bud = Chikko.this.getBuddyById(Integer.parseInt(packet.getParameter(15)));
							if (bud != null){
								bud.setStatus(Integer.parseInt(packet.getParameter(1)));
								if (listener != null){
									listener.onBuddyStatusChanged(bud);
								}
							}
						}
						else if (packet.getOperationCode() == OP_BUDDYIDLE){
							Buddy bud = Chikko.this.getBuddyById(Integer.parseInt(packet.getParameter(15)));
							if (bud != null){
								if (packet.getParameter(30).equals("1")){
									bud.setStatus(Buddy.STATUS_IDLE);
								}
								else {
									bud.setStatus(Buddy.STATUS_AVAILABLE);
								}
								if (listener != null){
									listener.onBuddyStatusChanged(bud);
								}
							}
						}
						else if (packet.getOperationCode() == OP_BUDDYADDNOTIFY){
							
						}
						else {
							System.out.println(packet.toString());
						}
						
						
					}
					catch (SocketException e){}
					catch (IOException e){}
				}
			}
		}
	}
	
}
